/**
 * 
 */
package br.com.vfcfw.commons.util;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.log4j.Logger;

/**
 * Classe utilitária para construção de instruções SQL.
 * 
 * @author Vitor Franco do Carmo (vitor.carmo@cpmbraxis.com)
 * @since 18/11/2011
 */
public class SQLText {

    private static final Logger LOGGER = Logger.getLogger(SQLText.class);

    private static final String AND = " AND ";

    private static final String WHERE = " WHERE ";

    private static final String OR = " OR ";

    private static final String COMMA = ", ";

    private boolean possuiRestricao = false;

    private final Map<String, Object> parametrosNomeados = new LinkedHashMap<String, Object>();

    private final StringBuilder sentencaSQL = new StringBuilder();

    /**
     * Default constructor of the class.
     */
    public SQLText() {

        super();
    }

    /**
     * Adiciona uma sentença SQL.
     * 
     * @param queryString Sentença SQL.
     * @return {@link SQLText}
     */
    public SQLText adicionar(String queryString) {

        sentencaSQL.append(queryString);
        return this;
    }

    /**
     * Adiciona um parâmetro de restrição na sentença SQL.
     * 
     * @param nomeParametro Nome do parâmetro.
     * @param valor Valor do parâmetro.
     * @return {@link SQLText}
     */
    public SQLText adicionarParametro(String nomeParametro, Object valor) {

        parametrosNomeados.put(nomeParametro, valor);
        setPossuiRestricao(true);

        return this;
    }

    /**
     * Adiciona uma restrição na sentença SQL se o valor informado não for nulo ou vazio. Adiciona o operador
     * <tt>AND</tt> se necessário.
     * 
     * @param queryString Sentença SQL.
     * @param nomeParametro Nome do parâmetro.
     * @param valor Valor do parâmetro.
     * @return {@link SQLText}
     */
    public SQLText adicionarRestricaoAnd(String queryString, String nomeParametro, Object valor) {

        if (possuiValor(valor)) {
            if (!isPossuiRestricao() && parametrosNomeados.isEmpty()) {
                sentencaSQL.append(WHERE);
            } else {
                sentencaSQL.append(AND);
            }
            setPossuiRestricao(true);
            sentencaSQL.append(queryString);
            parametrosNomeados.put(nomeParametro, valor);
        }
        return this;
    }

    /**
     * Adiciona uma restrição na sentença SQL. Adiciona o operador <tt>AND</tt> se necessário.
     * 
     * @param queryString Sentença SQL.
     * @return {@link SQLText}
     */
    public SQLText adicionarRestricaoAnd(String queryString) {

        if (!isPossuiRestricao() && parametrosNomeados.isEmpty()) {
            sentencaSQL.append(WHERE);
        } else {
            sentencaSQL.append(AND);
        }
        setPossuiRestricao(true);
        sentencaSQL.append(queryString);
        return this;
    }

    /**
     * Adiciona o operador <tt>AND</tt> e uma restrição na sentença SQL. A cláusula <tt>WHERE</tt> já tem que estar
     * presente na sentença SQL, não será adicionada por este método.
     * 
     * @param queryString Sentença SQL.
     * @param nomeParametro Nome parâmetro.
     * @param valor Valor parâmetro.
     * @return {@link SQLText}
     */
    public SQLText adicionarRestricaoAndSemWhere(final String queryString, final String nomeParametro,
        final Object valor) {

        if (possuiValor(valor)) {
            sentencaSQL.append(AND);
            sentencaSQL.append(queryString);
            parametrosNomeados.put(nomeParametro, valor);
            setPossuiRestricao(true);
        }
        return this;
    }

    /**
     * Adiciona uma restrição na sentença SQL se o valor informado não for nulo ou vazio. Adiciona o operador
     * <tt>OR</tt> se necessário.
     * 
     * @param queryString Sentença SQL.
     * @param nomeParametro Nome do parâmetro.
     * @param valor Valor do parâmetro.
     * 
     * @return {@link SQLText}
     */
    public SQLText adicionarRestricaoOr(String queryString, String nomeParametro, Object valor) {

        if (possuiValor(valor)) {
            if (!isPossuiRestricao() && parametrosNomeados.isEmpty()) {
                sentencaSQL.append(WHERE);
            } else {
                sentencaSQL.append(OR);
            }
            setPossuiRestricao(true);
            sentencaSQL.append(queryString);
            parametrosNomeados.put(nomeParametro, valor);
        }
        return this;
    }

    /**
     * Adiciona uma restrição na sentença SQL. Adiciona o operador <tt>OR</tt> se necessário.
     * 
     * @param queryString Sentença SQL.
     * @return {@link SQLText}
     */
    public SQLText adicionarRestricaoOr(String queryString) {

        if (!isPossuiRestricao() && parametrosNomeados.isEmpty()) {
            sentencaSQL.append(WHERE);
        } else {
            sentencaSQL.append(OR);
        }
        setPossuiRestricao(true);
        sentencaSQL.append(queryString);
        return this;
    }

    /**
     * Adiciona a restrição IN na sentença SQL. Adiciona o operador <tt>AND</tt> se necessário.
     * 
     * @param queryString Sentença SQL.
     * @param nomesParametros Array com os nomes dos parâmetros.
     * @param valores Array com os valores dos parâmetros.
     * @return {@link SQLText}
     */
    public SQLText adicionarRestricaoAndIn(String queryString, String[] nomesParametros, Object[] valores) {

        if (possuiValor(valores)) {
            if (!isPossuiRestricao() && parametrosNomeados.isEmpty()) {
                sentencaSQL.append(WHERE);
            } else {
                sentencaSQL.append(AND);
            }

            StringBuilder varParams = new StringBuilder();

            for (int i = 0; i < valores.length; i++) {
                varParams.append(nomesParametros[i]);
                varParams.append(COMMA);
                parametrosNomeados.put(nomesParametros[i], valores[i]);
            }
            varParams.delete(varParams.length() - 2, varParams.length());

            setPossuiRestricao(true);
            sentencaSQL.append(queryString);
            sentencaSQL.append(String.format(" IN (%s)", varParams.toString()));
        }
        return this;
    }

    /**
     * Adiciona a restrição NOT IN na sentença SQL. Adiciona o operador <tt>AND</tt> se necessário.
     * 
     * @param queryString Sentença SQL.
     * @param nomesParametros Array com os nomes dos parâmetros.
     * @param valores Array com os valores dos parâmetros.
     * @return {@link SQLText}
     */
    public SQLText adicionarRestricaoAndNotIn(String queryString, String[] nomesParametros, Object[] valores) {

        if (possuiValor(valores)) {
            if (!isPossuiRestricao() && parametrosNomeados.isEmpty()) {
                sentencaSQL.append(WHERE);
            } else {
                sentencaSQL.append(AND);
            }

            StringBuilder varParams = new StringBuilder();

            for (int i = 0; i < valores.length; i++) {
                varParams.append(nomesParametros[i]);
                varParams.append(COMMA);
                parametrosNomeados.put(nomesParametros[i], valores[i]);
            }
            varParams.delete(varParams.length() - 2, varParams.length());

            setPossuiRestricao(true);
            sentencaSQL.append(queryString);
            sentencaSQL.append(String.format(" NOT IN (%s)", varParams.toString()));
        }
        return this;
    }

    /**
     * Adiciona a restrição IN na sentença SQL. Adiciona o operador <tt>OR</tt> se necessário.
     * 
     * @param queryString Sentença SQL.
     * @param nomesParametros Array com os nomes dos parâmetros.
     * @param valores Array com os valores dos parâmetros.
     * @return {@link SQLText}
     */
    public SQLText adicionarRestricaoOrIn(String queryString, String[] nomesParametros, Object[] valores) {

        if (possuiValor(valores)) {
            if (!isPossuiRestricao() && parametrosNomeados.isEmpty()) {
                sentencaSQL.append(WHERE);
            } else {
                sentencaSQL.append(OR);
            }

            StringBuilder varParams = new StringBuilder();

            for (int i = 0; i < valores.length; i++) {
                varParams.append(nomesParametros[i]);
                varParams.append(COMMA);
                parametrosNomeados.put(nomesParametros[i], valores[i]);
            }
            varParams.delete(varParams.length() - 2, varParams.length());

            setPossuiRestricao(true);
            sentencaSQL.append(queryString);
            sentencaSQL.append(String.format(" IN (%s)", varParams.toString()));
        }
        return this;
    }

    /**
     * Adiciona a restrição NOT IN na sentença SQL. Adiciona o operador <tt>OR</tt> se necessário.
     * 
     * @param queryString Sentença SQL.
     * @param nomesParametros Array com os nomes dos parâmetros.
     * @param valores Array com os valores dos parâmetros.
     * @return {@link SQLText}
     */
    public SQLText adicionarRestricaoOrNotIn(String queryString, String[] nomesParametros, Object[] valores) {

        if (possuiValor(valores)) {
            if (!isPossuiRestricao() && parametrosNomeados.isEmpty()) {
                sentencaSQL.append(WHERE);
            } else {
                sentencaSQL.append(OR);
            }

            StringBuilder varParams = new StringBuilder();

            for (int i = 0; i < valores.length; i++) {
                varParams.append(nomesParametros[i]);
                varParams.append(COMMA);
                parametrosNomeados.put(nomesParametros[i], valores[i]);
            }
            varParams.delete(varParams.length() - 2, varParams.length());

            setPossuiRestricao(true);
            sentencaSQL.append(queryString);
            sentencaSQL.append(String.format(" NOT IN (%s)", varParams.toString()));
        }
        return this;
    }

    /**
     * Adiciona uma sentença SQL se o valor do parâmetro não for nulo ou vazio.
     * 
     * @param queryString Sentença SQL.
     * @param valor Valor do parâmetro.
     * @return {@link SQLText}
     */
    public SQLText adicionarSePossuirValor(String queryString, Object valor) {

        if (possuiValor(valor)) {
            sentencaSQL.append(queryString);
        }
        return this;
    }

    /**
     * Adiciona um parâmetro de restrição na sentença SQL se ele não for nulo ou vazio.
     * 
     * @param nomeParametro Nome do parâmetro.
     * @param valor Valor do parâmetro.
     * @return {@link SQLText}
     */
    public SQLText adicionarParametroSePossuirValor(String nomeParametro, Object valor) {

        if (possuiValor(valor)) {
            parametrosNomeados.put(nomeParametro, valor);
        }
        setPossuiRestricao(true);
        return this;
    }

    /**
     * Adiciona a sentença SQL e seus parâmetros de outro {@link SQLText} na sentença SQL atual.
     * 
     * @param sqlText {@link SQLText} Sentença SQL e seus parâmetros.
     * @return {@link SQLText}
     */
    public SQLText adicionarSentencaSQLCompleta(SQLText sqlText) {

        sentencaSQL.append(sqlText.obterSentencaSQL());
        parametrosNomeados.putAll(sqlText.obterParametrosNomeados());

        if (!parametrosNomeados.isEmpty()) {

            setPossuiRestricao(true);
        }
        return this;
    }

    /**
     * Retorna a sentença SQL construída.
     * 
     * @return Sentença SQL construída.
     */
    public String obterSentencaSQL() {

        LogUtil.info(LOGGER, String.format("Sentença SQL criada pelo SQLText:\n", toStringForDebug()));

        return sentencaSQL.toString();
    }

    /**
     * Retorna o {@link Map} com os parâmetros nomeados.
     * 
     * @return {@link Map} com os parâmetros nomeados.
     */
    public Map<String, Object> obterParametrosNomeados() {

        return parametrosNomeados;
    }

    /**
     * Verifica se a sentença SQL possui alguma restrição.
     * 
     * @return <tt>TRUE</tt> se houver alguma restrição, <tt>FALSE</tt> caso contrário.
     */
    private boolean isPossuiRestricao() {

        return possuiRestricao;
    }

    /**
     * Especifica se a sentença SQL possui alguma restrição.
     * 
     * @param <tt>TRUE</tt> para definir que houve uma restrição, <tt>FALSE</tt> caso contrário.
     */
    private void setPossuiRestricao(boolean haveRestriction) {

        this.possuiRestricao = haveRestriction;
    }

    /**
     * {@inheritDoc}
     * 
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {

        return obterSentencaSQL();
    }

    /**
     * Retorna a sentença SQL construída com os valores dos parâmetros.
     */
    public String toStringForDebug() {

        String queryString = obterSentencaSQL();

        for (Entry<String, Object> entry : parametrosNomeados.entrySet()) {
            if (entry.getValue() != null) {
                queryString = queryString.replaceAll(entry.getKey(), entry.getValue().toString());
            } else {
                queryString = queryString.replaceAll(entry.getKey(), null);
            }
        }
        return queryString;
    }

    /**
     * Verifica se o valor é nulo ou vazio.
     * 
     * @param valor Valor.
     * @return <tt>TRUE</tt> se não for <b>nulo</b> ou <b>vazio</b>, <tt>FALSE</tt> caso contrário.
     */
    public boolean possuiValor(final Object valor) {

        return StringUtil.possuiValor(valor);
    }

}
